package server;

/* ActionsImpl
 * ---------------------------------------------------
 * Written by
 * Rebecca Van Bussel 6271316
 * October 24, 2013
 */

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;

import centralRepository.CentralRepository;
import centralRepository.ServerObjectImpl;

import server.ActionsPOA;

public class ActionsImpl extends ActionsPOA implements Runnable {

	private LogActionsServer la;
	private static String[] serverNames={"SPVM", "SPL", "SPB"};
	//private static int[] portNums={2020, 2021, 2022};
	private static String[] recordCounts=new String[serverNames.length];
	private HashStructure hs;
	private int myPortNum;
	private String stationName;
	private AtomicInteger mrNum= new AtomicInteger(10000);
	private AtomicInteger crNum= new AtomicInteger(10000);
	private Object syncLock = new Object();
	private ServerObjectImpl serverObject = null;
	private Broadcaster broadcaster = null;

	private Date dNow = new Date( );
	private SimpleDateFormat ft = new SimpleDateFormat ("E yyyy.MM.dd 'at' hh:mm:ss a zzz");

	private static final int BUFFER_LENGTH=211;

	public ActionsImpl(int port, String stationName, ServerObjectImpl serverObject) {
		this.myPortNum=port;
		this.hs = new HashStructure();
		this.la = new LogActionsServer(stationName);
		this.serverObject = serverObject;
		this.stationName = stationName;
		
		//Thread thread = new Thread(broadcaster);
		//thread.start();
		//System.out.println("Started thread");	
		
	}
	
	public ActionsImpl() {
		
	}

	public void run() {
		handlePackets();
	}

	private void handlePackets() {
		//Servers wait for request packets to come in
		try {
			byte[] inBuf=new byte[BUFFER_LENGTH];
			DatagramSocket server = new DatagramSocket(myPortNum);
			DatagramPacket inPacket = new DatagramPacket(inBuf, inBuf.length);
			DatagramPacket outPacket;

			while (true) {
				//receive message
				server.receive(inPacket);
				
				InetAddress clientAddress = inPacket.getAddress();
				int clientPort = inPacket.getPort();
				byte[] outBuf = new byte[BUFFER_LENGTH];
				
				
				//verify request and prepare reply
				if (new String(inPacket.getData(), 0, 10).equals("Send tally")) {
					outBuf = getCount().getBytes();
					if(serverObject.isLeader()){
						System.out.println("inside " + stationName + " record count is " + getCount() + " Port num: " + this.myPortNum);
					}
					outPacket = new DatagramPacket(outBuf, 0, outBuf.length, clientAddress, clientPort);

					//send reply
					server.send(outPacket);
				}


				else if (new String(inPacket.getData(), 0, 12).equals("Transfer rec")) {

					String data = new String(inPacket.getData(), 0, inPacket.getLength());

					String fname=data.substring(12, 27).trim();
					String lname=data.substring(27, 42).trim();
					String address=data.substring(42, 96).trim();
					Date date=new Date(data.substring(96, 126).trim());
					String location= data.substring(126, 180).trim();
					String status = data.substring(180, 195).trim();
					char type = data.charAt(195);
					String officerId = data.substring(202, 210).trim();

					System.out.println(fname+" "+lname+" "+address+" "+date.toString()+" "+location+" "+status+" "+type+" "+officerId);

					String confirm = createRecord(fname, lname, address, date, location, status, type, officerId);

					//If record successfully created, method returns "Record RECORD_ID created"
					if (confirm.startsWith("Record")) {
						//get id from message
						String newId=confirm.substring(7, 14);
						outBuf = ("Successful add"+newId).getBytes();
					}
					else
						outBuf = "Unsuccessful add".getBytes();

					outPacket = new DatagramPacket(outBuf, 0, outBuf.length, clientAddress, clientPort);

					//send reply
					server.send(outPacket);
				}
				
				else if (new String(inPacket.getData(), 0, 13).equals("Create CriRec") || new String(inPacket.getData(), 0, 13).equals("Create MisRec")) {
					String data = new String(inPacket.getData(), 0, inPacket.getLength());

					String fname=data.substring(13, 27).trim();
					String lname=data.substring(27, 42).trim();
					String address=data.substring(42, 96).trim();
					Date date=new Date(data.substring(96, 126).trim());
					String location= data.substring(126, 180).trim();
					String status = data.substring(180, 195).trim();
					char type = data.charAt(195);
					String officerId = data.substring(202, 211).trim();

					System.out.println("CREATING: " + fname+" "+lname+" "+address+" "+date.toString()+" "+location+" "+status+" "+type+" "+officerId);

					String confirm = createRecord(fname, lname, address, date, location, status, type, officerId);

					//If record successfully created, method returns "Record RECORD_ID created"
					if (confirm.startsWith("Record")) {
						//get id from message
						String newId=confirm.substring(7, 14);
						outBuf = ("Successful add"+newId).getBytes();
					}
					else
						outBuf = "Unsuccessful add".getBytes();

					outPacket = new DatagramPacket(outBuf, 0, outBuf.length, clientAddress, clientPort);

					//send reply
					server.send(outPacket);
				}
				
				else if (new String(inPacket.getData(), 0, 13).equals("Transf DelRec")){
					String data = new String(inPacket.getData(), 0, inPacket.getLength());
					String officerId;
					String id = data.substring(13, 20);
					String remoteStationServerName = data.substring(20, 23);
					int indexStart = 23;
					if(remoteStationServerName.equals("SPV")){
						remoteStationServerName = data.substring(20, 24);
						indexStart = 24;
					}
					if(indexStart == 24){
						officerId = data.substring(indexStart, 31);
						if(officerId.equals("SPV"))
							officerId = data.substring(indexStart, 32);
					}
					else{
						officerId = data.substring(indexStart, 30);
						if(officerId.equals("SPV"))
							officerId = data.substring(indexStart, 31);
					}
					
					System.out.println("TRANSFERRING: " + id + " " + remoteStationServerName + " "+officerId);

					String confirm = transferRecord(id, remoteStationServerName, officerId);
					//If record successfully transfered, method returns "Record RECORD_ID created"
					if (confirm.startsWith("Record")) {
						//get id from message
						String newId=confirm.substring(7, 14);
						outBuf = ("Successful transfer"+newId).getBytes();
					}
					else
						outBuf = "Unsuccessful transfer".getBytes();

					outPacket = new DatagramPacket(outBuf, 0, outBuf.length, clientAddress, clientPort);

					//send reply
					server.send(outPacket);
				}
				
				else if (new String(inPacket.getData(), 0, 13).equals("Edited Record")){
					String data = new String(inPacket.getData(), 0, inPacket.getLength());
					String[] splitData = data.split(";");
					
					String confirm = editRecord(splitData[1], splitData[2], splitData[3], splitData[4]);
					
					System.out.println("Editing: " + splitData[1] + " " + splitData[2] + " " + splitData[3] + " " + splitData[4]);

					//If record successfully created, method returns "Record RECORD_ID created"
					if (confirm.startsWith("Record")) {
						//get id from message
						outBuf = confirm.getBytes();
					}
					else
						outBuf = "Unsuccessful edit".getBytes();

					outPacket = new DatagramPacket(outBuf, 0, outBuf.length, clientAddress, clientPort);

					//send reply
					server.send(outPacket);
					
				}

				//for rollback if problems creating duplicate record
				else if (new String(inPacket.getData(), 0, 13).equals("Delete record")) {
					String data = new String(inPacket.getData(), 0, inPacket.getLength());
					Record temp = hs.removeRecord(data.substring(13, 20));
					if (temp!=null&&temp.getId().equals(data.substring(13, 20)))
						outBuf = "Successful del".getBytes();
					else
						outBuf = "Unsuccessful delete".getBytes();
					outPacket = new DatagramPacket(outBuf, 0, outBuf.length, clientAddress, clientPort);
					server.send(outPacket);

				}

			}

		} catch (IOException e) {
			e.printStackTrace();
		}

	}

	//********************************************************************************
	//Methods as dictated by Assignment
	//********************************************************************************

	public String createCRecord (String fname, String lname, String address, long lastDate, String lastLocation, String status, String officerId) {
		String response=createRecord (fname, lname, address, new Date(lastDate), lastLocation, status, 'C', officerId);
		if(serverObject.isLeader()) {
			String id = response.substring(7,14);
			Record record = hs.getRecord(id);
			System.out.println(record.stringRecord());
			byte[] outBuf=("Create CriRec"+record.stringRecord()).getBytes();
			broadcaster.addToQueue(outBuf);
		}
		
		return response;
	}

	public String createMRecord (String fname, String lname, String address, long lastDate, String lastLocation, String status, String officerId) {
		String response=createRecord (fname, lname, address, new Date(lastDate), lastLocation, status, 'M', officerId);
		if(serverObject.isLeader()) {
			String id = response.substring(7,14);
			Record record = hs.getRecord(id);
			System.out.println(record.stringRecord());
			byte[] outBuf=("Create MisRec"+record.stringRecord()).getBytes();
			broadcaster.addToQueue(outBuf);
		}
		return response;
	}

	public String transferRecord(String id, String remoteStationServerName, String officerId) {

		String newId="";
		byte[] outBuf = new byte[BUFFER_LENGTH];
		InetAddress address;
		DatagramSocket client = null;
		try {
			address = InetAddress.getByName(CentralRepository.getLeader(serverObject).getHostname());
			client = new DatagramSocket();
			client.setSoTimeout(10000); //set timeout to 10 seconds
		} catch (UnknownHostException e1) {
			la.addAction(ft.format(dNow)+" Officer "+officerId+" failed to transfer record "+id+" to "+remoteStationServerName);
			return "Error getting local host information.";
		} catch (SocketException e) {
			la.addAction(ft.format(dNow)+" Officer "+officerId+" failed to transfer record "+id+" to "+remoteStationServerName);
			return "Error creating socket.";
		}

		//find port num
		int outPort = 0;
		for (int i=0;i<serverNames.length;++i)
			if (serverNames[i].equals(remoteStationServerName))
				outPort = Integer.parseInt(CentralRepository.getLeader(remoteStationServerName).getPortNum());
		//start synchronization
		synchronized(syncLock) {

			Record deleted = null;

			//delete record from this server
			deleted=hs.removeRecord(id);
			//add this to the broadcaster
			if(serverObject.isLeader()) {
				byte[] outBuf2=("Transf DelRec"+id+remoteStationServerName+officerId).getBytes();
				broadcaster.addToQueue(outBuf2);
			}
			//if record id null, record does not exist
			if (deleted==null) {
				la.addAction(ft.format(dNow)+" Officer "+officerId+" failed to transfer record "+id+" to "+remoteStationServerName);
				return "Record does not exist. No changes made.";
			}

			//if record ids don't match something weird went down. This shouldn't ever happen, but just in case...
			if (!deleted.getId().equals(id)) {
				//re-add erroneously deleted record
				hs.addRecord(deleted);
				la.addAction(ft.format(dNow)+" Officer "+officerId+" failed to transfer record "+id+" to "+remoteStationServerName);
				return "Error deleting existing record. No changes made.";
			}

			//send record
			outBuf=("Transfer rec"+deleted.stringRecord()).getBytes();
			DatagramPacket outPacket = new DatagramPacket(outBuf, 0, outBuf.length, address, outPort);
			try {
				client.send(outPacket);
			} catch (IOException e1) {
				//not sent properly: re-add record to hs on this server
				hs.addRecord(deleted);
				la.addAction(ft.format(dNow)+" Officer "+officerId+" failed to transfer record "+id+" to "+remoteStationServerName);
				return "Error sending record to "+remoteStationServerName+". No changes made.";
			}

			//receive reply
			byte[] inBuf = new byte[BUFFER_LENGTH];
			DatagramPacket inPacket = new DatagramPacket(inBuf, inBuf.length);
			try {
				client.receive(inPacket);				
				//check if received properly and get new record ID
				if (new String(inPacket.getData(), 0, 14).equals("Successful add")) {
					newId=new String(inPacket.getData(), 14, 21);				
					la.addAction(ft.format(dNow)+" Officer "+officerId+" transferred record "+id+" to server "+remoteStationServerName+", new id: "+newId);
					//SUCCESS!
					return "Record "+id+" successfully transferred. New id at "+remoteStationServerName+": "+newId;
				}

			} catch (SocketTimeoutException e) {
				System.out.println("Socket timeout when communicating with "+remoteStationServerName);
			} catch (IOException e) { } 

			//not received properly: re-add record to hs on this server
			hs.addRecord(deleted);
			la.addAction(ft.format(dNow)+" Officer "+officerId+" failed to transfer record "+id+" to "+remoteStationServerName);
			return "Error sending record to "+remoteStationServerName+". No changes made.";

		}//end synchronization
	}

	public String editRecord (String lname, String id, String newStatus, String officerId) {
		//check that status matches type
		if (!checkStatus(id.charAt(0), newStatus)) {
			la.addAction(ft.format(dNow)+" Officer "+officerId+" failed to edit record "+id+", wrong status type");
			return "Error editing record, wrong status type";
		}
		synchronized (syncLock) {
			if (hs.editRecord(lname, id, newStatus)) {
				la.addAction(ft.format(dNow)+" Officer "+officerId+" successfully edited record "+id);
				
				//add this to the broadcaster
				if(serverObject.isLeader()) {
					byte[] outBuf2=("Edited Record"+ ";" + lname + ";" + id + ";" + newStatus + ";" + officerId).getBytes();
					broadcaster.addToQueue(outBuf2);
				}
				
				return "Record "+id+" edited";
			}
		}
		la.addAction(ft.format(dNow)+" Officer "+officerId+" failed to edit record "+id);
		return "Error editing record. Either record does not exist or it has been transferred.";
	}

	public String getRecordCounts(String officerId) {
		String returnVal="";
		if(serverObject.isLeader()){
			System.out.println("IN GET GET COUNT BEFORE THREAD: " + getCount() + " Port is: " + this.myPortNum); 
		}
		int myIndex = 0;
		for (int i=0;i<serverNames.length;++i) {
			if(!serverNames[i].equals(this.stationName)){
				System.out.println("Handling " + i);
				Thread t = new Thread(new getTallies(i));
				t.start();
				try {
					t.join(1000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			else{
				myIndex = i;
			}
		}
		recordCounts[myIndex] = "\n"+this.stationName+":\t"+this.getCount();
		//compile replies into return string
		for (int i=0;i<recordCounts.length;++i) {
			returnVal+=recordCounts[i];
		}
		la.addAction(ft.format(dNow)+" Officer "+officerId+" checked record tallies");
		return returnVal;
	}

	//Inner class to handle getting tallies concurrently
	private static class getTallies implements Runnable {
		private int index;
		public getTallies(int i) {
			System.out.println("In get Tallies for " + i);
			this.index=i;
		}
		public void run() {
			try {
				//send message
				byte[] buf = ("Send tally").getBytes();
				InetAddress address = InetAddress.getLocalHost();
				DatagramSocket client = new DatagramSocket();
				int port = Integer.parseInt(CentralRepository.getLeader(serverNames[index]).getPortNum());
				DatagramPacket packet = new DatagramPacket(buf, buf.length, address, port);
				
				client.send(packet);

				//receive reply
				byte[] inBuf = new byte[BUFFER_LENGTH];
				DatagramPacket inPacket = new DatagramPacket(inBuf, inBuf.length);
				client.receive(inPacket);

				String data = new String(inPacket.getData(), 0, inPacket.getLength());
				System.out.println("index " + index + ", data " + data);
				//store reply
				recordCounts[index]="\n"+serverNames[index]+":\t"+data;
			} catch (UnknownHostException e) {
				e.printStackTrace();
			} catch (SocketException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	private String getCount() {

		return String.valueOf(hs.getcRecordCount()+hs.getmRecordCount());
	}

	private String createRecord(String fname, String lname, String address, Date lastDate, String lastLocation, String status, char type, String officerId) {
		if ((status.equals("Missing")||status.equals("Found")))
			type = 'M';
		if ((status.equals("Captured")||status.equals("At large")))
			type = 'C';
		
		//check that status matches type
		if (!checkStatus(type, status)) {
			la.addAction(ft.format(dNow)+" Officer "+officerId+" failed to created record, wrong status " + status + " for type " + type);
			return "Error creating record, wrong status type";
		}
		Record record=null;

		//add record to hash table
		synchronized(syncLock) {
			if (type=='C')
				record=new Record(fname, lname, address, lastDate, lastLocation, status, officerId, type, crNum.get());
			if (type=='M')
				record=new Record(fname, lname, address, lastDate, lastLocation, status, officerId, type, mrNum.get());


			if (hs.addRecord(record)) {
				la.addAction(ft.format(dNow)+" Officer "+officerId+" successfully created record "+record.getId());
				if (type=='C')
					crNum.incrementAndGet();
				if (type=='M')
					mrNum.incrementAndGet();
				return "Record "+record.getId()+" created";
			}
		}
		la.addAction(ft.format(dNow)+" Officer "+officerId+" failed to create record.");
		return "Error creating and storing record";

	}

	//method to check that status matches record type (to comply with assignment specifications)
	private boolean checkStatus(char type, String status) {
		if (type=='M'&&(status.equals("Missing")||status.equals("Found")))
			return true;
		if (type=='C'&&(status.equals("Captured")||status.equals("At large")))
			return true;
		return false;
	}

	//login
	public void startSession(String officerId) {
		if (serverObject==null) {
			serverObject=CentralRepository.getLeader(officerId.substring(0, officerId.length()-4));
			this.myPortNum=Integer.parseInt(serverObject.getPortNum());
			this.hs = new HashStructure();
			this.la = new LogActionsServer(stationName);
			if(serverObject.isLeader()){
				this.broadcaster = new Broadcaster(serverObject);
				broadcaster.setLeaderPortNumber(serverObject.getPortNum());
			}
			this.stationName = serverObject.getStation();
		}
		la.addAction(ft.format(dNow)+" Officer "+officerId+" logged in");
	}

	//logout
	public void endSession(String officerId) {
		la.addAction(ft.format(dNow)+" Officer "+officerId+" logged out");
	}

	public String getAllRecords(String officerId) {
		la.addAction(ft.format(dNow)+" Officer "+officerId+" viewed all records");
		return hs.toString();
	}
	
	public String getMyRecords(String officerId) {
		la.addAction(ft.format(dNow)+" Officer "+officerId+" viewed their records");
		return hs.getOfficerRecords(officerId);
	}

}
